β

Как стать автором
Погнали в космос?
Посмотри в Календарь, вдруг сегодня есть мероприятие?

Моя лентаВсе потоки

lowitea 24 дек 2021 в 09:47

Интерфейсы в Go — как красиво выстрелить себе в ногу

8 мин

41K

Программирование*Go*

Первый очерк из цикла приключений в мире сусликов.

С этой статьи начинается серия небольших рассказов о необычных подводных камнях, которые можно встретить в начале разработки на Go. В статьях будут примеры кода, будьте с ними аккуратнее - не все из них будут компилироваться и работать, читайте внимательно комментарии, везде будет указано, на какой строке происходит ошибка. Также в блоках кода везде табуляция заменена на пробелы - это сделано намеренно, чтобы статьи выглядели у всех одинаково.

Начинать рассказывать я буду издалека, с самых основ, но это необходимо для полного понимания. В конце же рассказа, будет самое интересное, но всё равно стоит читать его с самого начала.

Пара слов обо мне. Я всю жизнь занимаюсь разработкой программного обеспечения, в основном в сфере WEB, успел познакомиться с многими языками программирования и поработать в разных крупных компаниях. Сейчас руковожу разработкой в компании NUT.Tech, мы там делаем классные и интересные вещи. В данный момент в основном разработка в отделе построена вокруг Go, поэтому о нём я вам и расскажу.

Статьи серии:

Интерфейсы в Go - как красиво выстрелить себе в ногу

Нарезаем массивы правильно в Go

...

Пожалуй, начнём. Вы когда-нибудь задумывались, что такое interface? Ну, то есть, не ключевое слово синтаксиса, а что это такое в рантайме? Как выглядит его проинстанцированный объект? А, главное, каким свойством обладает при сравнении с nil? Нет? Тогда устраивайтесь поудобнее, я сейчас вам всё расскажу.

Начнём с определения интерфейса из спецификации языка:

An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is nil.

Мой вольный перевод:

Тип интерфейса определяет множество методов, называющееся интерфейс. Переменная типа интерфейса может хранить значение любого типа с набором методов, который является любым надмножеством интерфейса. Говорят, что такой тип реализует интерфейс. Значение неинициализированной переменной типа интерфейса равно nil.

На мой взгляд, хорошее абстрактное описание, которое не очень сильно помогает осознать интерфейсы на практике, но в целом даёт общее понимание. Очень советую пойти в документации в раздел Interface_types - там много примеров и более подробное описание.

Но этого поста бы не было, будь там вся необходимая информация ;) Допустим, мы уже понимаем, что переменная с интерфейсом в качестве типа это некий объект, который с точки зрения типизации обязан обладать всеми перечисленными в указанном интерфейсе методами.

Необходимо также понимать, что переменные с интерфейсом в качестве типа бессмысленны сами по себе, они не создаются и не используются в отрыве от обычных типов. И именно переменные обычных типов, превращаются в переменные интерфейсного типа. А с точки зрения компилятора в переменную интерфейсного типа может превратится любая переменная обычного типа, у которой её тип содержит все заявленные в интерфейсе методы. Хотя могут быть и дополнительные методы - реализация интерфейса от этого не сломается. Понимаю, всё несколько сложно и запутано, но дальше будут примеры и станет понятнее.

А что происходит, например, когда мы переменную определенного, конкретного типа, превращаем в другую переменную с интерфейсным типом? В Golang, как впрочем и многих других статически типизированных языках, у переменной можно легально получить доступ только к тем атрибутам и методам, которые существуют у типа этой переменной. В случае преобразования переменная получает определенный интерфейсный тип. Одна из очевидных проблем, которую мы встречаем, - потеря доступа ко всем атрибутам нашего конкретного типа и к его методам, которые не были указаны в интерфейсе.

Посмотрим на конкретном примере:

package main

import (
"fmt"
"reflect"
)

// создадим некоторый тип Человек,
// который умеет спать, есть и работать
type Man struct{}

func (m Man) Sleep() {}
func (m Man) Eat() {}
func (m Man) Work() {}

// и тип Пёс, который у нас умеет только лаять
type Dog struct{}

func (d Dog) Bark() {}

// теперь мы хотим понять, кто может стать программистом,
// для этого мы определяем интерфейс Programmer,
// и определяем в нём метод Work
// этим мы ограничиваем количество объектов,
// которые смогут быть программистами
// так случилось, что в нашем коде, чтобы стать программистом,
// достаточно уметь работать :)
type Programmer interface {
Work()
}

func main() {
// итак, теперь посмотрим, как работать с нашими типами и интерфейсом

// для начала создадим некоторого конкретного человека
Vasiliy := Man{}
// и посмотрим на его тип
fmt.Printf("%s\n", reflect.TypeOf(Vasiliy).String()) // main.Man

// также создадим Василию верного друга
Sharik := Dog{}
fmt.Printf("%s\n", reflect.TypeOf(Sharik).String()) // main.Dog

// что ж, теперь нужно проверить, кто же,
// Шарик или Василий нам лучше подойдёт на роль программиста
// для этого создадим абстрактного работника с типом Programmer
// и попробуем к нему присвоить наши объекты
var worker Programmer
worker = Sharik
// увы, этот код не будет скомпилирован,
// компилятор выведет следующую ошибку
// cannot use Sharik (type Dog) as type Programmer in assignment:
// Dog does not implement Programmer (missing Work method)

// теперь мы знаем, Шарик пока не может быть программистом,
// придётся им стать Василию
worker = Vasiliy // этот код компилируется и работает

// в целом, этого достаточно для базового понимания интерфейсов,
// но дальше будет ещё кое-что интересное

// проверим тип нашего работника теперь
fmt.Printf("%s\n", reflect.TypeOf(worker).String()) // main.Man
// в целом выглядит логично: мы сказали,
// что работником будет Василий, у которого тип Man,
// но раз уж так, давайте попробуем вызвать
// метод Sleep у нашего работника
worker.Sleep()
// worker.Sleep undefined (type Programmer has no field or method Sleep)
// увы, этот код не будет работать,
// так как на самом деле worker != Vasiliy
// типом переменной worker является интерфейс Programmer,
// который не содержит метод Sleep()
}

Что же происходит, и почему пакет reflect определяет тип нашего воркера как Man, которым на самом деле он не является? Чтобы это понять, нужно погрузиться в недра исходного кода нашего "суслика" и посмотреть, чем представлен там тип interface:

// https://golang.org/src/runtime/runtime2.go

type iface struct {
tab *itab
data unsafe.Pointer
}

Если не вдаваться в подробности, то в поле tab у нас хранится информация о конкретном типе объекта, который был преобразован в интерфейс. А в поле data - ссылка на реальную область памяти, в которой лежат данные изначального объекта, в нашем случае Василия. Поэтому библиотека reflect, когда хочет получить настоящий исходный тип объекта интерфейса, идёт в это поле tab и получает информацию о типе там.

У нас остаётся возможность преобразовать наш объект интерфейса обратно к исходному типу, для этого у нас есть синтаксис утверждений типа, выглядит он следующим образом:

m, ok := worker.(Man)
fmt.Printf("%#v\n", m) // main.Man{}
fmt.Printf("%t\n", ok) // true

Можно использовать и без переменной ok, но в таком случае Вы получите панику, если в интерфейсе окажется несоответствующий тип.

Окей, с этим вроде разобрались, идём дальше.

Как нам говорит документация: The value of an uninitialized variable of interface type is nil. То есть наша переменная интерфейсного типа может принимать nil. Ну это же супер! Если отмотать немного назад, то можно увидеть, как мы изначально инициализировали нашего работника - var worker Programmer. Если верить документации, то в этот момент наш worker был равен nil. Доверяй, но проверяй:

func main() {
var worker Programmer
fmt.Printf("%#v\n", reflect.TypeOf(worker)) // <nil>
// и так, тут уже тип нашего воркера не main.Man,
// как это было при присвоении туда Василия, а nil
// ну хорошо, давайте ещё сделаем проверку,
// действительно ли наш пустой работник равен nil
fmt.Printf("%t\n", worker == nil) // true
// отлично, и правда nil
}

Но мы пойдём дальше: а что если попробовать создать пустую переменную типа *Man и преобразовать её к интерфейсу? Пробуем:

func main() {
var man *Man
fmt.Printf("%#v\n", man) // (*main.Man)(nil)
var worker Programmer
fmt.Printf("%#v\n", worker) // <nil>

worker = man
fmt.Printf("%#v\n", worker) // (*main.Man)(nil)
// наш воркер изменил тип, он больше не совсем nil,
// а nil от типа *main.Man
// что же это для нас значит, давайте смотреть

fmt.Printf("%t\n", man == nil) // true
// отлично, наш man равен nil,
// мы же туда ничего не положили и ссылка равна nil
fmt.Printf("%t\n", worker == man) // true
// ну тоже хорошо, мы положили в переменную
// worker переменную man,
// и как мы уже убеждались ранее, они равны

// ну и понятно, по законам математики,
// если x = 0, а y = x, значит y = 0
fmt.Printf("%t\n", worker == nil) // false
// а вот и нет, это вам Go, а не математика
}

Давайте разбираться, почему так происходит. Как я уже говорил, объект интерфейса в Go содержит два поля: tab с информацией о конкретном типе и data, где лежит ссылка на сами данные. И вот, по правилам Go, интерфейс может быть равен nil только если оба этих поля не определены.

Давайте смотреть, что у нас получается, когда мы преобразуем переменную man в интерфейс:

func main() {
var worker Programmer
fmt.Printf("%s\n", reflect.ValueOf(worker)) // <invalid reflect.Value>
// рефлект не может получить значение из переменной worker,
// потому что его просто нет,
// то есть поле `data` у нашего интерфейса не задано
fmt.Printf("%v\n", reflect.TypeOf(worker)) // <nil>
// как не может получить и тип,
// так как он тоже не задан в поле `tab`
fmt.Printf("%t\n", worker == nil) // true
// worker равен nil

var man *Man
fmt.Printf("%t\n", man == nil) // true
// переменная man равняется nil

worker = man

fmt.Printf("%s\n", reflect.ValueOf(worker).Elem())
// <invalid reflect.Value>
// после присвоения переменной worker переменной man
// мы точно также не можем получить значение через reflect
// так как man у нас не содержит каких-либо данных, он равен `nil`
fmt.Printf("%v\n", reflect.TypeOf(worker)) // *main.Man
// а вот с типом интереснее мы из переменной worker
// получили тип переменной man, то есть *main.Man
// соответственно поле `tab` у нашего интерфейса уже явно не пустое
// и поэтому переменная worker больше не будет равна nil
fmt.Printf("%t\n", worker == nil) // false
}

Как итог, мы имеем интерфейс с заполненным полем tab, и проверка на равенство с nil всегда будет возвращать false. Несмотря на то, что изначально переменная возвращала true при сравнении с nil.

Описано ли это поведение в документации к нашему суслику? - Нет. Ну точнее не совсем. В спецификации языка это не описано, но это есть в FAQ по языку: https://golang.org/doc/faq#nil_error. И там как раз рассмотрена одна из распространённых ошибок, связанная с этим поведением интерфейсов: когда мы из функции возвращаем ссылку на объект нашей кастомной ошибки. Данная ссылка, в свою очередь, может быть nil, если ошибка в функции не произошла. И, когда происходит преобразование нашего объекта кастомной ошибки к интерфейсу error, этот новый преобразованный объект уже никогда больше не будет равен nil, и все проверки if err != nil {} перестанут работать корректно.

Приведу пример чуть более подробный чем в faq:

package main

import "fmt"

// создаём свой собственный тип ошибки,
// обычно это бывает нужно чтобы передавать дополнительные данные
// вместе с ошибкой, так например сделано в библиотеке twirp
type CustomError struct{}

// этот метод нам нужен, чтобы реализовать интерфейс error
func (CustomError) Error() string { return "CustomError" }

// а в этой функции мы будем возвращать нашу ошибку, либо nil
func raiseError(raise bool) *CustomError {
if raise == true {
return &CustomError{}
}
return nil
}

func main() {
// для начала нам нужно инициализировать переменную,
// которая будет принимать результат из функции raiseError
var err error

// пробуем вернуть из нашей функции ошибку
err = raiseError(true)
fmt.Println(err != nil) // true
// отлично, err у нас действительно не равен nil

// а теперь вызовем функцию таким образом, чтобы она вернула nil
err = raiseError(false)
fmt.Println(err != nil) // true
// после преобразования в интерфейс, наш nil уже не очень то и nil
}

На этом мой очерк заканчивается, читайте о других интересных моментах из мира Go в следующих сериях и будьте аккуратны с интерфейсами.

Теги:

go

golang

программирование

Хабы:

Программирование

Go

+16

131

5
+5

19

Карма

0

Рейтинг

@lowitea

Freelance sommelier

Подписаться

Комментарии 5
+5

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.1K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.6K

Кейс

Перевод

+27

20

2
+2

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.4K

Мнение

+22

17

59
+59

lokosst 13 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

23

0

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.1K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.6K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.5K

+16

5

6
+6

psygalaxy 13 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.8K

Обзор

+16

30

9
+9

kubelet 16 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

Красим папки и распределяем роли: тестируем новую версию менеджера паролей Пассворк 7.0
Турбо

Показать еще

Минуточку внимания

Турбо

Делаем сисадмина сильнее с помощью менеджера паролей

Опрос

Да начнётся битва: выбираем лучший IT-бренд работодателя

Промо

Неважно, что ты выберешь — с Промокодусом будет дешевле

Курсы

Программирование на языке Python. Базовый курс
11 августа 2025Учебный центр «Микротест»

Курс «Основы Linux»
18 августа 2025Академия Codeby

Проектирование и реализация очередей в брокерах RabbitMQ и Apache Kafka
24 августа 2025Systems Education

Systems Analyst Bootcamp: Проектировщик корпоративных информационных систем
4 сентября 2025Systems Education

Инженер по тестированию: с нуля до Junior+
19 сентября 2025QA Studio

Больше курсов на Хабр Карьере

Читают сейчас

Как превратить ноутбук в полноценное рабочее место не хуже стационарного ПК

12K

71
+71

Нашел ЛЯП в игре 5букв от Т-банка, который позволяет со 100% вероятностью угадать скрытое слово

54K

115
+115

Как выйти из IT?.. и пойти слесарем на завод. Моя попыточка дауншифтинга [4\5]

1.4K

12
+12

Моё мини-исследование про продолжительность жизни в России

6.4K

59
+59

GPT-5 получит модуль для борьбы с «галлюцинациями» — The Information

725

3
+3

Красим папки и распределяем роли: тестируем новую версию менеджера паролей Пассворк 7.0
Турбо

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Ближайшие события

7 августа

Мем-пати: как построить IT-бренд через вирусный контент и юмор

Москва

Больше событий в календаре

Маркетинг

13 августа

Как повысить эффективность врача в стоматологии на 25% с помощью ИИ?

Онлайн

Больше событий в календаре

Менеджмент

13 августа

Тест-драйв Хабра для маркетологов и пиарщиков

Москва

Больше событий в календаре

Маркетинг

16 августа

«ИТ-летник» — конференция от Совкомбанк Технологий

Казань

Больше событий в календаре

Разработка

Аналитика

3 сентября

«GoCloud Tech 2025» — IT-конференция про облака и AI

Москва • Онлайн

Больше событий в календаре

Разработка

18 – 19 сентября

Конференция по корпоративному онлайн-обучению iSpring Days 2025

Москва • Онлайн

Больше событий в календаре

Администрирование

Менеджмент

Другое

19 сентября

Премия для технологических стартапов Startech.Awards 2025

Москва • Онлайн

Больше событий в календаре

Другое

30 сентября

VII премия в сфере фармацевтического маркетинга и креатива MedMen Awards 2025

Онлайн

Больше событий в календаре

Маркетинг

Другое

30 октября

Премия «Проксима»

Москва

Больше событий в календаре

Маркетинг

Реклама

АО СЕЛЕКТЕЛ

ИНН 7810962785

erid: 2VSb5zDBFAH

1 000 000 стартапам

Кешбэк 30% на год в Selectel!

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr